package com.ar4j;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.ObjectStreamException;
import java.io.Serializable;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

import com.ar4j.type.ITypeConverter;
import com.ar4j.util.ClassUtils;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

/**
 * A portable wrapper holding an active record's state in JSON format. 
 * This is usefull for shipping information about a record over the wire using RMI or another
 * transport facility. 
 */
public class ActiveRecordJsonWrapper implements Serializable, Externalizable {
  private static final String[] EXCLUDED_EQUALS_HASHCODE_FIELDS = { "config" };
  
  private ActiveRecord<?> record;
  
  /**
   * No arg constructor for use during deserialization
   */
  public ActiveRecordJsonWrapper() {
    // EMPTY HERE
  }
  
  /**
   * Construct a wrapper for the given active record.
   * NOTE: the wrapper maintains a reference to the given record and will output
   * up to date values if the record is modified after this constructor is called.
   */
  public ActiveRecordJsonWrapper(ActiveRecord<?> record) {
    this.record = record;
  }
  
  public ActiveRecordJsonWrapper(String jsonString) {
    loadRecordFromJsonString(jsonString);
  }
  
  public ActiveRecord<?> getRecord() {
    return record;
  }
  
  @SuppressWarnings("unchecked")
  public <E extends ActiveRecord<E>> E getRecord(Class<E> domainClass) {
    ActiveRecord<?> out = getRecord();
    if(!out.getBaseClass().equals(domainClass)) {
      throw new IllegalArgumentException("Base class of current record does not match base class of wrapped record, expected: " + domainClass + ", got: " + out.getBaseClass());
    }
    
    return (E) getRecord();
  }
  
  /**
   * Used during deserialization, makes sure that an active record instance is returned
   * when deserializing rather than the wrapper itself.
   * 
   * @return the active record for which this wrapper was created
   * @throws ObjectStreamException
   */
  public Object readResolve() throws ObjectStreamException {
    return record;
  }
  
  @Override
  public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    loadRecordFromJsonString(in.readUTF());
  }
  
  @Override
  public void writeExternal(ObjectOutput out) throws IOException {
    out.writeUTF(toString());
  }
  
  @Override
  public boolean equals(Object obj) {
    return EqualsBuilder.reflectionEquals(this, obj, EXCLUDED_EQUALS_HASHCODE_FIELDS);
  }

  @Override
  public int hashCode() {
    return HashCodeBuilder.reflectionHashCode(record, EXCLUDED_EQUALS_HASHCODE_FIELDS);
  }

  @SuppressWarnings("unchecked")
  @Override
  public String toString() {
    JsonObject root = new JsonObject();
    root.addProperty("factory", record.getFactoryName());
    root.addProperty("baseClass", record.getBaseClass().getName());
    
    JsonObject properties = new JsonObject();
    root.add("properties", properties);
    for(String property : record.getPropertyNames()) {
      ITypeConverter<Object, Object> converter = (ITypeConverter<Object, Object>) record.getPropertyTypeConverter(property);
      Object value = record.getProperty(property);
      if(converter != null) {
        value = converter.convertToStorage(value);
      }
      
      String jsonValue = ClassUtils.coerceTypeToString(value);
      properties.addProperty(property, jsonValue);
    }
    
    
    String json = new Gson().toJson(root);
    return json;
  }
  
  /**
   * Creates a new active record with the classname specified in the json string and 
   * populates its values with the properties object from the same string.
   */
  @SuppressWarnings("unchecked")
  private <S> void loadRecordFromJsonString(String jsonString) {
    try {
      JsonObject root = new JsonParser().parse(jsonString).getAsJsonObject();
      String factory = root.getAsJsonPrimitive("factory").getAsString();
      String baseClassName = root.getAsJsonPrimitive("baseClass").getAsString();
      Class<?> domainClass = Class.forName(baseClassName);
      
      this.record = NamedSingletonActiveRecordFactory.getFactory(factory).getActiveRecord((Class) domainClass);
      
      JsonObject properties = root.get("properties").getAsJsonObject();
      for(String property : record.getPropertyNames()) {
        if(!properties.has(property)) {
          continue;
        }
        
        Object value = properties.get(property).getAsString();
        
        ITypeConverter<?, S> converter = (ITypeConverter<?, S>) record.getPropertyTypeConverter(property);
        if(converter != null) {
          value = ClassUtils.coerceValueToType(value, converter.getStorageType());
          value = converter.convertFromStorage((S) value);
        } else {
          Class<?> objectType = record.getPropertyType(property);
          value = ClassUtils.coerceValueToType(value, objectType);
        }
        
        record.setProperty(property, value);
      }
      
    } catch(Throwable e) {
      throw new RuntimeException("Could not parse record from string: " + jsonString, e);
    }
  }
  
}
